﻿#region Using References

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;

#endregion

namespace Beaker
{
	/// <summary>
	/// A base class for objects of which the properties can be observable.
	/// </summary>
	public abstract class ObservableObject
		: INotifyPropertyChanged, INotifyPropertyChanging
	{
		private PropertyChangingEventHandler _propertyChanging;

		/// <summary>
		/// Occurs before a property will change.
		/// </summary>
		public event PropertyChangingEventHandler PropertyChanging
		{
			// based on:
			// http://stackoverflow.com/questions/3522361/add-delegate-to-event-thread-safety

			add
			{
				PropertyChangingEventHandler localHandler = _propertyChanging;
				PropertyChangingEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangingEventHandler)Delegate.Combine( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanging, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
			remove
			{
				PropertyChangingEventHandler localHandler = _propertyChanging;
				PropertyChangingEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangingEventHandler)Delegate.Remove( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanging, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
		}

		private PropertyChangedEventHandler _propertyChanged;

		/// <summary>
		/// Occurs after a property has changed.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged
		{
			// based on:
			// http://stackoverflow.com/questions/3522361/add-delegate-to-event-thread-safety

			add
			{
				PropertyChangedEventHandler localHandler = _propertyChanged;
				PropertyChangedEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangedEventHandler)Delegate.Combine( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanged, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
			remove
			{
				PropertyChangedEventHandler localHandler = _propertyChanged;
				PropertyChangedEventHandler originalHandler;
				do
				{
					originalHandler = localHandler;
					var combinedHandler = (PropertyChangedEventHandler)Delegate.Remove( originalHandler, value );
					localHandler = Interlocked.CompareExchange( ref _propertyChanged, combinedHandler, originalHandler );
				}
				while( localHandler != originalHandler );
			}
		}

		/// <summary>
		/// Sets the value of the property and raise NotifyPropertyChanged if changed.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="backingStore">The backing store that holds the value.</param>
		/// <param name="newValue">The new value to set to.</param>
		/// <param name="onChange">The action callback to call if the value actually has changed.</param>
		/// <param name="propertyName">Name of the property.</param>
		protected void SetAndRaiseIfChanged<T>( ref T backingStore, T newValue, Action<T, T> onChange = null, [CallerMemberName] string propertyName = null )
		{
			Contract.Requires( propertyName != null, "A property name is required" );
			Contract.Ensures( EqualityComparer<T>.Default.Equals( backingStore, newValue ) );

			bool hasChanged = !EqualityComparer<T>.Default.Equals( backingStore, newValue );
			if( hasChanged )
			{
				T oldValue = backingStore;

				RaisePropertyChanging( propertyName );
				backingStore = newValue;
				RaisePropertyChanged( propertyName );

				if( onChange != null )
					onChange( oldValue, newValue );
			}
		}

		/// <summary>
		/// Raises the PropertyChanging event.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		protected virtual void RaisePropertyChanging( [CallerMemberName] string propertyName = null )
		{
			Contract.Requires( propertyName != null, "A property name is required" );

			PropertyChangingEventHandler propertyChanging = _propertyChanging;
			if( propertyChanging != null )
				propertyChanging( this, new PropertyChangingEventArgs( propertyName ) );
		}

		/// <summary>
		/// Raises the PropertyChanged event.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		private void RaisePropertyChanged( [CallerMemberName] string propertyName = null )
		{
			Contract.Requires( propertyName != null, "A property name is required" );

			PropertyChangedEventHandler propertyChanged = _propertyChanged;
			if( propertyChanged != null )
				propertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
		}

		/// <summary>
		/// Raises the PropertyChanging event for the property with the given name.
		/// </summary>
		/// <typeparam name="T">The type of the property that will change.</typeparam>
		/// <param name="propertyExpression">An expression identifying the property that will change.</param>
		protected virtual void RaisePropertyChanging<T>( Expression<Func<T>> propertyExpression )
		{
			RaisePropertyChanging( GetPropertyName( propertyExpression ) );
		}

		/// <summary>
		/// Raises the PropertyChanging event for the property with the given name.
		/// 
		/// </summary>
		/// <typeparam name="T">The type of the property that has changed.</typeparam>
		/// <param name="propertyExpression">An expression identifying the property has changed.</param>
		protected virtual void RaisePropertyChanged<T>( Expression<Func<T>> propertyExpression )
		{
			RaisePropertyChanged( GetPropertyName( propertyExpression ) );
		}

		/// <summary>
		/// Get the name of a property from an expression.
		/// </summary>
		/// <typeparam name="T">The type of the property.</typeparam>
		/// <param name="propertyExpression">An expression returning the property's name.</param>
		/// <returns>
		/// The name of the property returned by the expression.
		/// </returns>
		/// <exception cref="ArgumentNullException">Thrown when the expression is null.</exception>
		/// <exception cref="ArgumentException">Thrown when the expression does not represent a property.</exception>
		protected string GetPropertyName<T>( Expression<Func<T>> propertyExpression )
		{
			if( propertyExpression == null )
				throw new ArgumentNullException( "propertyExpression", "Argument is not a property expression." );

			var memberExpression = propertyExpression.Body as MemberExpression;
			if( memberExpression == null )
				throw new ArgumentException( "Argument is not a property expression.", "propertyExpression" );

			var propertyInfo = memberExpression.Member as PropertyInfo;
			if( propertyInfo == null )
				throw new ArgumentException( "Argument is not a property expression.", "propertyExpression" );

			return propertyInfo.Name;
		}

	}
}
